/**
 * Copyright (c) 2007 Todd A. Fisher
 * You can redistribute it and/or modify it under the same terms as Ruby.
 * readings http://people.apache.org/~rooneg/talks/ruby-extensions/ruby-extensions.html
 * http://www.rubycentral.com/book/ext_ruby.html
 * http://www.rubyinside.com/how-to-create-a-ruby-extension-in-c-in-under-5-minutes-100.html
 * http://209.85.165.104/search?q=cache:rqTs2R2cW94J:www.iona.com/hyplan/vinoski/pdfs/IEEE-Ruby_Extensions.pdf+ruby+extensions&hl=en&ct=clnk&cd=10&gl=us
 * http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/86602
 */
#define DEBUG
#include "ruby.h"
#include "ext_help.h"
#include <assert.h>
#include <string.h>
#include <ctype.h>
#include "esi.h"

#define id_buffer_ready "@buffer_ready"
#define id_esi_tag_ready "@esi_tag_ready"

static VALUE mMongrel;
static VALUE mESI;
static VALUE cESIParser;


VALUE ESIParser_default_esi_tag_ready( VALUE self, VALUE tag, VALUE start_tag );
VALUE ESIParser_default_buffer_ready( VALUE self, VALUE buffer );

/**
 * The basic data structure we're dealing with...
 * {:ready_buffer => "", 
 *  :not_ready_buffer => "", 
 *  :pending_includes => [{ :src => "", :alt => "", 
 *                          :id => :Fixnum, 
 *                          :ttl => :Fixnum }] }
 */

/**
 * call-seq:
 *    parser.new ( {:buffer_ready => lambda {|buffer| },
 *                  :tag_ready => lambda {|tag| }
 *                 } ) -> parser
 *
 * Creates a new parser.
 */
VALUE ESIParser_init( VALUE self, VALUE options )
{
  VALUE buffer_ready_callback = Qnil, esi_tag_ready_callback = Qnil;

  if( options && !NIL_P(options) ){
    buffer_ready_callback = rb_hash_aref(options,rb_eval_string(":buffer_ready"));
    esi_tag_ready_callback = rb_hash_aref(options,rb_eval_string(":esi_tag_ready"));
  }

  if( !buffer_ready_callback || NIL_P(buffer_ready_callback) ){
    buffer_ready_callback = rb_intern("default_buffer_ready");
  }

  if( !esi_tag_ready_callback || NIL_P(esi_tag_ready_callback) ){
    esi_tag_ready_callback = rb_intern("default_esi_tag_ready");
  }

  rb_iv_set(self, id_buffer_ready, buffer_ready_callback);
  rb_iv_set(self, id_esi_tag_ready, esi_tag_ready_callback);

  return self;
}

/**
 * call-seq:
 *
 *      parser.buffer_ready = lambda{ |buffer| } -> lambda{ |buffer| }
 *
 * Change the buffer_ready method
 */
VALUE ESIParser_set_buffer_ready( VALUE self, VALUE callback )
{
  printf( "setting callback \n" );
  rb_iv_set(self, id_buffer_ready, callback);
  return callback;
}

/**
 * call-seq:
 *
 *      parser.tag_ready = lambda{ |tag,end_tag| } -> lambda{ |tag,end_tag| }
 *
 * Change the tag_ready method
 */
VALUE ESIParser_set_tag_ready( VALUE self, VALUE callback )
{
  rb_iv_set(self, id_esi_tag_ready, callback);
  return callback;
}

/**
 * call-seq:
 *
 *      parser.buffer_ready -> lambda{ |buffer| }
 *
 * Get the buffer_ready method
 */
VALUE ESIParser_get_buffer_ready( VALUE self )
{
  return rb_iv_get(self, id_buffer_ready);
}

/**
 * call-seq:
 *
 *      parser.tag_ready -> lambda{ |tag| }
 *
 * Get the tag_ready method
 */
VALUE ESIParser_get_tag_ready( VALUE self )
{
  return rb_iv_get(self, id_esi_tag_ready);
}

/* lame bsd doesn't define strndup */
static char *strdupN( const char *ptr, unsigned int length )
{
  char *buffer = (char*)malloc(sizeof(char)*(length+1));
  memcpy( buffer, ptr, length );
  buffer[length] = '\0';
  return buffer;
}

const char *ESI_START = "<esi:";
const unsigned int ESI_START_LENGTH = 5;
const char *ESI_END = "</esi:";
const unsigned int ESI_END_LENGTH = 6;

static unsigned int esi_scan( const char *ptr, 
                              unsigned int length, 
                              const char *seq_check, 
                              unsigned int seq_len, 
                              VALUE self )
{
  unsigned int buffer_length;
  char *esi_start = strnstr(ptr,seq_check,length);
  char *esi_end = NULL; // can be either /> or </esi:try>
  VALUE buffer;

  while( esi_start ){
    buffer_length = (esi_start - ptr);
    buffer = rb_str_new( ptr, buffer_length );

    // scan buffer for closing tags
    esi_scan( RSTRING(buffer)->ptr, RSTRING(buffer)->len, ESI_END, ESI_END_LENGTH, self );

    esi_end = strnstr( esi_start, ">", length );
    if( esi_end ){  // NULL check in case someone is being malicious
      // emit the esi tag for processing
      buffer_length = (esi_end - esi_start + 1);
      rb_funcall( ESIParser_get_tag_ready(self), rb_intern("call"), 2, rb_str_new( esi_start, buffer_length ), !strcmp( seq_check, ESI_START ) );
      length -= buffer_length;
      ptr = esi_end + 1;
    }

    esi_start = strnstr( ptr, seq_check, length );
  }

  if( length > 0 && seq_len == ESI_END_LENGTH ){
    // emit data ready for the client
    rb_funcall( ESIParser_get_buffer_ready(self), rb_intern("call"), 1, rb_str_new( ptr, strlen( ptr ) ) );
    length = 0;
  }

  return length;
}

/**
 * call-seq:
 *    parser.parse(buffer) -> nil
 * Parses the document calling buffer_ready as the passed in buffer appears with no ESI tags.
 * For each esi tag found that is an include tag esi_tag_ready is called.  It is expected that
 * esi_tag_ready call set_include, which passes the contents of esi_tag_ready.  buffer_ready will
 * not be called if an include is not fullfilled, Qnil is valid buffer to pass in set_include so
 * esi_tag_ready can fail gracefully
 */
VALUE ESIParser_parse( VALUE self, VALUE buffer )
{
  unsigned int overflow;

  overflow = esi_scan( RSTRING(buffer)->ptr, RSTRING(buffer)->len, ESI_START, ESI_START_LENGTH, self );
  if( overflow ){
    esi_scan( RSTRING(buffer)->ptr + overflow, RSTRING(buffer)->len - overflow, ESI_END, ESI_END_LENGTH, self );
  }

  return Qnil;
}

VALUE ESIParser_finish( VALUE self )
{
  return Qnil;
}

/**
 * call-seq:
 *    parser.default_esi_tag_ready(src,alt,ttl,id) -> nil
 *
 * params:
 *   tag => the markup of the unparsed esi tag, e.g. <esi:try>  or <esi:include src="foo" max-age="bar"/>
 *   start_tag => boolean, true when the tag is a start tag false otherwise
 *
 * Called when an ESI include tag is encountered during the parse
 */
VALUE ESIParser_default_esi_tag_ready( VALUE self, VALUE tag, VALUE start_tag )
{
  return Qnil;
}

/**
 * call-seq:
 *    parser.default_buffer_ready(buffer) -> nil
 *
 * any content in the parse stream that appears without any ESI tags or has been merged with resulting includes
 * via calls from set_include
 */
VALUE ESIParser_default_buffer_ready( VALUE self, VALUE buffer )
{
  return Qnil;
}


void Init_esi()
{
  mMongrel = rb_define_module("Mongrel");
  mESI = rb_define_module_under(mMongrel,"ESI");
  cESIParser = rb_define_class_under(mESI, "Parser", rb_cObject);
  rb_define_method(cESIParser, "initialize", ESIParser_init,1);
  rb_define_method(cESIParser, "parse", ESIParser_parse,1);
  rb_define_method(cESIParser, "finish", ESIParser_finish,0);
  rb_define_method(cESIParser, "default_buffer_ready", ESIParser_default_buffer_ready,1);
  rb_define_method(cESIParser, "default_esi_tag_ready", ESIParser_default_esi_tag_ready,2);
  rb_define_method(cESIParser, "buffer_ready=", ESIParser_set_buffer_ready,1);
  rb_define_method(cESIParser, "tag_ready=", ESIParser_set_tag_ready,1);
  rb_define_method(cESIParser, "buffer_ready", ESIParser_get_buffer_ready,0);
  rb_define_method(cESIParser, "tag_ready", ESIParser_get_tag_ready,0);
    
}


#ifndef HAVE_STRNSTR
// see: http://svn.netlabs.org/libc/browser/trunk/src/emx/src/lib/bsd/string/strnstr.c?rev=771
char *strnstr( const char *s, const char *find, size_t slen )
{
  char c, sc;
  size_t len;

  if ((c = *find++) != '\0') {
          len = strlen(find);
          do {
                  do {
                          if ((sc = *s++) == '\0' || slen-- < 1)
                                  return (NULL);
                  } while (sc != c);
                  if (len > slen)
                          return (NULL);
          } while (strncmp(s, find, len) != 0);
          s--;
  }
  return ((char *)s);
}
#endif
